﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace pk3DS.Core.CTR;

public static partial class FileFormat
{
    internal const string defaultExtension = "bin";
    internal static readonly string[] validEXT = ["BCH"];

    public static string Guess(string path)
    {
        using var br = new BinaryReader(File.OpenRead(path));
        return Guess(br);
    }

    public static string Guess(byte[] data)
    {
        using var br = new BinaryReader(new MemoryStream(data));
        return Guess(br);
    }

    public static string Guess(MemoryStream ms, bool start = true)
    {
        using var br = new BinaryReader(ms);
        return Guess(br, start);
    }

    public static string Guess(BinaryReader br, bool start = true)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.

        if (start) // Seek to top of stream if requested
            br.BaseStream.Position = 0;

        // Guess Extension
        if (GuessMini(br, out var ext))
            Console.WriteLine("Mini Packed File detected, extension type " + ext);
        else if (GuessHeaderedDARC(br, out ext))
            Console.WriteLine("Headered DARC File detected, extension type " + ext);
        else if (GuessBCLIM(br, out ext))
            Console.WriteLine("BCLIM File detected, extension type " + ext);
        else if (GuessLZ11(br, out ext))
            Console.WriteLine("LZ11 Compressed File detected, extension type " + ext);
        else if (Guess4CHAR(br, out ext))
            Console.WriteLine("4CHAR File detected, extension type " + ext);
        else if (Guess3CHAR(br, out ext))
            Console.WriteLine("3CHAR File detected, extension type " + ext);
        else ext = defaultExtension; // default

        // Return BaseStream position to the start.
        br.BaseStream.Position = position;
        return "." + ext;
    }

    public static bool GuessMini(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            // check for 2char container extensions
            ushort magic = br.ReadUInt16();
            ushort count = br.ReadUInt16();
            br.BaseStream.Position = 4 + (4 * count);
            if (br.ReadUInt32() == br.BaseStream.Length)
            {
                ext += (char)magic & 0xFF;
                ext += (char)magic << 8;
            }
        }
        catch { }
        // Return BaseStream position to the start.
        br.BaseStream.Position = position;

        return ext.Length > 0;
    }

    public static bool GuessHeaderedDARC(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            byte[] magic = Encoding.ASCII.GetBytes(br.ReadChars(4));
            int count = BitConverter.ToUInt16(magic, 0);
            br.BaseStream.Position = position + 4 + (0x40 * count);
            uint tableval = br.ReadUInt32();
            br.BaseStream.Position += 0x20 * tableval;
            while (br.PeekChar() == 0) // seek forward
                br.ReadByte();
            if (br.ReadUInt32() == 0x63726164)
                ext = "darc";
        }
        catch { }
        // Return BaseStream position to the start.
        br.BaseStream.Position = position;

        return ext.Length > 0;
    }

    public static bool GuessBCLIM(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            br.BaseStream.Position = br.BaseStream.Length - 0x28;
            if (br.ReadUInt32() == 0x4D494C43)
            {
                br.BaseStream.Position = br.BaseStream.Length - 0x4;
                if (br.ReadUInt32() == br.BaseStream.Length - 0x28)
                    ext = "bclim";
            }
        }
        catch { }
        // Return BaseStream position to the start.
        br.BaseStream.Position = position;

        return ext.Length > 0;
    }

    public static bool GuessLZ11(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            int type = br.PeekChar();
            if (type != 0x11)
                return false;
            byte[] sizeBytes = new byte[3];
            br.Read(sizeBytes, 0, 3);

            int decompressedSize = sizeBytes[0] | sizeBytes[1] << 8 | sizeBytes[2];
            if (decompressedSize > br.BaseStream.Length && decompressedSize < br.BaseStream.Length * 10) // assuming 10x compression isn't feasible
                ext = "lz"; // really weak LZ detection, at most 16MB
        }
        catch { }
        br.BaseStream.Position = position;
        return ext.Length > 0;
    }

    public static bool Guess4CHAR(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            byte[] magic = Encoding.ASCII.GetBytes(br.ReadChars(4));

            Regex r = PatternAZ09();
            ext = Encoding.ASCII.GetString(magic);
            // Return BaseStream position to the start.
            br.BaseStream.Position = position;

            return r.IsMatch(ext) && ext.Length == 4;
        }
        catch { }
        br.BaseStream.Position = position;
        return false;
    }

    public static bool Guess3CHAR(BinaryReader br, out string ext)
    {
        long position = br.BaseStream.Position; // Store current position to reset after.
        ext = ""; // Reset extension
        try
        {
            byte[] magic = Encoding.ASCII.GetBytes(br.ReadChars(3));

            ext = Encoding.ASCII.GetString(magic);
            // Return BaseStream position to the start.
            br.BaseStream.Position = position;

            return validEXT.Contains(ext);
        }
        catch { }
        br.BaseStream.Position = position;
        return false;
    }

    [GeneratedRegex("^[a-zA-Z0-9]*$")]
    private static partial Regex PatternAZ09();
}